#
# Copyright 2009 Red Hat, Inc. and/or its affiliates.
#
# Licensed to you under the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.  See the files README and
# LICENSE_GPL_v2 which accompany this distribution.
#


import os
import logging
import types
import threading
import time

import metadata
import storage_exception as se
import misc

from config import config

DOMAIN_MNT_POINT = 'mnt'
DOMAIN_META_DATA = 'dom_md'
DOMAIN_IMAGES = 'images'
# Domain's metadata volume name
METADATA = "metadata"
# (volume) meta data slot size
METASIZE = 512
# Domain metadata slot size (it always takes the first slot)
SD_METADATA_SIZE = 2048
MAX_DOMAIN_DESCRIPTION_SIZE = 50

LEASES = "leases"
IDS = "ids"
INBOX = "inbox"
OUTBOX = "outbox"

LEASES_SIZE = "2G"
IDS_SIZE = "8M"
INBOX_SIZE = "16M"
OUTBOX_SIZE = "16M"

# Storage Domain Types
UNKNOWN_DOMAIN = 0
NFS_DOMAIN = 1
FCP_DOMAIN = 2
ISCSI_DOMAIN = 3
LOCALFS_DOMAIN = 4
CIFS_DOMAIN = 5

# use only upper case for values - see storageType()
DOMAIN_TYPES = {UNKNOWN_DOMAIN:'UNKNOWN', NFS_DOMAIN:'NFS', FCP_DOMAIN:'FCP',
                ISCSI_DOMAIN:'ISCSI', LOCALFS_DOMAIN:'LOCALFS',
                CIFS_DOMAIN:'CIFS'}

# Storage Domains Statuses: keep them capitalize
#DOM_UNINITIALIZED_STATUS = 'Uninitialized'
#DOM_DESTROYED_STATUS = 'Destroyed'
DEPRECATED_DOM_INACTIVE_STATUS = 'Inactive'
#DOM_ERROR_STATUS = 'Error'
DOM_UNKNOWN_STATUS = 'Unknown'
DOM_ATTACHED_STATUS = 'Attached'
DOM_UNATTACHED_STATUS = 'Unattached'
DOM_ACTIVE_STATUS = 'Active'

DOMAIN_STATUSES = [DOM_UNKNOWN_STATUS, DOM_ATTACHED_STATUS, DOM_UNATTACHED_STATUS, DOM_ACTIVE_STATUS]
DEPRECATED_STATUSES = {DEPRECATED_DOM_INACTIVE_STATUS: DOM_ATTACHED_STATUS}

DOMAIN_TRANSITIONS = {DOM_ATTACHED_STATUS: [DOM_UNATTACHED_STATUS, DOM_ATTACHED_STATUS, DEPRECATED_DOM_INACTIVE_STATUS, DOM_ACTIVE_STATUS],
                        DEPRECATED_DOM_INACTIVE_STATUS: [DOM_UNATTACHED_STATUS, DOM_ATTACHED_STATUS, DEPRECATED_DOM_INACTIVE_STATUS, DOM_ACTIVE_STATUS],
                        DOM_UNATTACHED_STATUS: [DOM_ATTACHED_STATUS, DEPRECATED_DOM_INACTIVE_STATUS],
                        DOM_ACTIVE_STATUS: [DOM_ATTACHED_STATUS, DEPRECATED_DOM_INACTIVE_STATUS, DOM_ACTIVE_STATUS]}
# Domain Role
MASTER_DOMAIN = 'Master'
REGULAR_DOMAIN = 'Regular'
# Domain Class
DATA_DOMAIN = 1
ISO_DOMAIN = 2
BACKUP_DOMAIN = 3
DOMAIN_CLASSES = {DATA_DOMAIN:'Data', ISO_DOMAIN:'Iso', BACKUP_DOMAIN:'Backup'}

# Metadata keys
VERSION = "VERSION"
SDUUID = "SDUUID"
TYPE = "TYPE"
ROLE = "ROLE"
DESCRIPTION = "DESCRIPTION"
CLASS = "CLASS"

# Lock related metadata keys
LOCK_POLICY = 'LOCKPOLICY'
LOCK_RENEWAL_INTERVAL_SEC = 'LOCKRENEWALINTERVALSEC'
LEASE_TIME_SEC = 'LEASETIMESEC'
IO_OP_TIMEOUT_SEC = 'IOOPTIMEOUTSEC'
LEASE_RETRIES = 'LEASERETRIES'

SDMETADATA_VERSION_0 = "0"
SDMETADATA_VERSION = SDMETADATA_VERSION_0
KNOWN_SDMETADATA_VERSIONS = [SDMETADATA_VERSION_0]

DOMAINS = "POOL_DOMAINS"
POOL = "POOL_UUID"
POOL_DESCRIPTION = "POOL_DESCRIPTION"
LVER = "POOL_SPM_LVER"
SPM_ID = "POOL_SPM_ID"
MASTER_VER = "MASTER_VERSION"

ISO_IMAGE_UUID = '11111111-1111-1111-1111-111111111111'
BLANK_UUID = '00000000-0000-0000-0000-000000000000'

SDMETAKEYS = [VERSION, SDUUID, TYPE, ROLE, DESCRIPTION, CLASS,
    LOCK_POLICY, LOCK_RENEWAL_INTERVAL_SEC, LEASE_TIME_SEC, IO_OP_TIMEOUT_SEC,
    LEASE_RETRIES]

POOLMETAKEYS = [DOMAINS, POOL, POOL_DESCRIPTION, LVER, SPM_ID, MASTER_VER]

# For now just use combined list
METAKEYS = SDMETAKEYS + POOLMETAKEYS

MASTER_DIR_NAME = 'master'
VMS_DIR_NAME = 'vms'
TASKS_DIR_NAME = 'tasks'

def validateSDStateTransition(sdUUID, currState, nextState):
    if nextState not in DOMAIN_TRANSITIONS[currState]:
        raise se.StorageDomainStateTransitionIllegal(sdUUID, currState, nextState)

def validateSDDeprecatedStatus(status):
    if not status.capitalize() in DEPRECATED_STATUSES:
        raise se.StorageDomainStatusError(status)
    return DEPRECATED_STATUSES[status.capitalize()]

def validateSDStatus(status):
    if not status.capitalize() in DOMAIN_STATUSES:
        raise se.StorageDomainStatusError(status)

def storageType(t):
    if isinstance(t, types.StringTypes):
        t = t.upper()
    if t in DOMAIN_TYPES.values():
        return t
    try:
        return type2name(int(t))
    except:
        raise se.StorageDomainTypeError(str(t))

def type2name(domType):
    try:
        return DOMAIN_TYPES[domType]
    except IndexError:
        return None

def name2type(name):
    for (k, v) in DOMAIN_TYPES.iteritems():
        if v == name.upper():
            return k
    return None

def class2name(domClass):
    try:
        return DOMAIN_CLASSES[domClass]
    except IndexError:
        return None

def name2class(name):
    for (k, v) in DOMAIN_CLASSES.iteritems():
        if v == name:
            return k
    return None

def sizeStr2Int(size_str):
    if size_str.endswith("M") or size_str.endswith("m"):
        size = int(size_str[:-1]) * (1 << 20)
    elif size_str.endswith("G") or size_str.endswith("g"):
        size = int(size_str[:-1]) * (1 << 30)
    else:
        size = int(size_str)

    return size

class SDMetadata(metadata.Metadata):
    """
    Common Storage Domain Metadata implementaion
    """
    metakeys = METAKEYS

    def _validate(self, md):
        """
        Validate the correctness of the Storage Domain metadata
        """
        valid = True
        versioned = False
        for i in md:
            if i.find("=") < 0:
                continue
            pair = i.split("=")
            # Everything should be in form PARAM=VALUE
            if len(pair) != 2:
                msg = "Malformed parameter %s" % (i)
                self.log.warning(str(se.MetaDataValidationError(msg)))
                valid = False
            # The PARAM (KEY) should be known
            if pair[0] not in self.metakeys:
                msg = "KEY %s is not registered" % (pair[0])
                self.log.warning(str(se.MetaDataValidationError(msg)))
                valid = False
            if pair[0] == VERSION:
                versioned = True
                # Check VERSION to be correct
                if pair[1] not in KNOWN_SDMETADATA_VERSIONS:
                    msg = "Unknown metadata version %s" % (pair[1])
                    self.log.warning(str(se.MetaDataValidationError(msg)))
                    valid = False
        if not versioned:
            msg = "No VERSION key in metadata"
            self.log.warning(str(se.MetaDataValidationError(msg)))
            valid = False

        return valid


class StorageDomain:
    log = logging.getLogger("irs")
    storage_repository = config.get('irs', 'repository')
    mdBackupVersions = config.get('irs','md_backup_versions')
    mdBackupDir = config.get('irs','md_backup_dir')

    def __init__(self, sdUUID):
        self.sdUUID = sdUUID
        self.domaindir = None
        self._metadata = None
        self._lock = threading.Lock()
        self.stat = None
        self._lastMetadataCheck = 0

    def __del__(self):
        if self.stat:
            threading.Thread(target=self.stat.stop).start()
            self.stat = None


    @classmethod
    def create(cls, sdUUID, domainName, domClass, typeSpecificArg):
        """
        Create a storage domain. The initial status is unattached.
        The storage domain underlying storage must be visible (connected)
        at that point.
        """
        pass


    def produceVolume(self, imgUUID, volUUID=None):
        """
        Produce a type specific volume object
        """
        pass


    def getVolumeClass(self):
        """
        Return a type specific volume generator object
        """
        pass


    def createVolume(self, isStopping, imgUUID, size, volFormat, preallocate, diskType, volUUID, desc, srcImgUUID, srcVolUUID):
        """
        Create a new volume
        """
        pass


    def getMDPath(self):
        if self.domaindir:
            return os.path.join(self.domaindir, DOMAIN_META_DATA)
        return None

    def initSPMlease(self):
        """
        Initialize SPM leases
        """
        pass


    def selftest(self):
        """
        Run internal self test
        """
        return True


    def validate(self):
        """Validate that the storage domain is accessible.
        """
        pass


    def invalidate(self):
        """
        Make sure that storage domain is inaccessible
        """
        pass

    def validateMaster(self):
        """Validate that the master storage domain is correct.
        """
        pass

    def createMasterTree(self, log=False):
        """
        """
        # Build new 'master' tree
        vmsDir = os.path.join(self.domaindir, MASTER_DIR_NAME, VMS_DIR_NAME)
        tasksDir = os.path.join(self.domaindir, MASTER_DIR_NAME, TASKS_DIR_NAME)
        for dir in [vmsDir, tasksDir]:
            if not os.path.exists(dir):
                os.makedirs(dir) # FIXME remove if not a pdir

    def disconnect(self):
        '''
        Do any cleanups upon disconnection
        '''
        self.log.debug("SD.disconnect: sd %s" % (self.sdUUID))


    def activate(self):
        """Activate a storage domain that is already a member in a storage pool.
        """
        if self.isBackup():
            self.mountMaster()
            self.createMasterTree()

    def deactivate(self):
        """Deactivate a storage domain.
        """
        if self.isBackup():
            self.unmountMaster()


    def format(self):
        """
        Format detached storage domain.
        This removes all data from the storage domain.
        """
        pass

    def getAllImages(self):
        """Fetch the list of the Image UUIDs
        """
        pass

    def getIsoList(self, extension):
        """Get list of all ISO/Floppy images
            'extension' - '.iso'/'.floppy' for ISO/Floppy images
        """
        pass

    def setDescription(self, descr):
        """ Set storage domain description
            'descr' - domain description
        """
        self.log.info("(StorageDomain.setDescription) sdUUID=%s descr=%s",
            self.sdUUID, descr)
        self.setMetaParam(DESCRIPTION, descr)

    def getInfo(self):
        """
        Get storage domain info
        """
        info = {}
        info['uuid'] = self.sdUUID
        info['type'] = self.getMetaParam(TYPE)
        info['class'] = self.getMetaParam(CLASS)
        info['name'] = self.getMetaParam(DESCRIPTION)
        info['role'] = self.getMetaParam(ROLE)
        info['pool'] = self.getMetaParam(POOL)
        info['lver'] = self.getMetaParam(LVER)
        info['spm_id'] = self.getMetaParam(SPM_ID)
        info['master_ver'] = self.getMetaParam(MASTER_VER)
        return info

    def getStats(self):
        """
        """
        pass

    def mountMaster(self):
        """
        Mount the master metadata file system. Should be called only by SPM.
        """
        pass


    def unmountMaster(self):
        """
        Unmount the master metadata file system. Should be called only by SPM.
        """
        pass


    def extendVolume(self, volumeUUID, size, isShuttingDown=None):
        pass


    def Metadata(self, md=None, cache=True):
        """
        Unified Metadata accessor/mutator
        """
        if (time.time() - self._lastMetadataCheck) > config.getfloat('irs', 'sd_metadata_lifetime_sec'):
            cache = False

        if md:
            self._metadata.metadata = md
        else:
            if not cache:
                # Signal to metadata object that caching is not desired
                self._metadata.clearcache()
                self._lastMetadataCheck = time.time()
            return self._metadata.metadata

    def getMetadata(self, cache=True):
        md = {}
        for item in self.Metadata(cache=cache):
            if item.find("=") < 0:
                continue
            k, v = item.split('=')
            md[k.strip()] = v.strip()

        return md

    def getMetaParam(self, key, cache=True):
        value = ''
        for i in self.Metadata(cache=cache):
            if i.find("=") < 0:
                continue
            k, v = i.split('=')
            if key == k.strip():
                value = v.strip()
                break

        return value

    def getStorageType(self):
        return name2type(self.getMetaParam(TYPE))

    def getDomainType(self):
        return name2class(self.getMetaParam(CLASS))

    def getRemotePath(self):
        pass

    def getMasterVer(self):
        return int(self.getMetaParam(MASTER_VER))

    def setMetaParam(self, key, value):
        """
        Set new meta data KEY=VALUE pair
        """
        try:
            self._lock.acquire()

            # Backup old md (rotate old backup files)
            misc.rotateFiles(self.mdBackupDir, self.sdUUID, self.mdBackupVersions)
            oldMd = [i + '\n' for i in self.Metadata()]
            open(os.path.join(self.mdBackupDir, self.sdUUID), "w").writelines(oldMd)

            # First filter out any possible previous param
            metadata = [line for line in self.Metadata() if not line.startswith(key + "=")]
            # Now add new value
            metadata.append("%s=%s" % (key, value))

            self.Metadata(metadata)
        finally:
            self._lock.release()

    def refresh(self):
        pass

    def extend(self, devlist):
        pass

    def isMaster(self):
        return self.getMetaParam(ROLE).capitalize() == MASTER_DOMAIN

    def isISO(self):
        return name2class(self.getMetaParam(CLASS)) == ISO_DOMAIN

    def isBackup(self):
        return name2class(self.getMetaParam(CLASS)) == BACKUP_DOMAIN

    def isData(self):
        return name2class(self.getMetaParam(CLASS)) == DATA_DOMAIN

    def checkImages(self, spUUID):
        import image
        badimages = {}
        imglist = self.getAllImages()
        for img in imglist:
            try:
                imgstatus = image.Image().check(sdUUID=self.sdUUID, spUUID=spUUID, imgUUID=img)
                if imgstatus["imagestatus"]:
                    badimages[img] = imgstatus
            except Exception, e:
                self.log.info("(sd.checkDomain): sp %s sd %s: image check for img %s failed: %s" % (spUUID, self.sdUUID, img, str(e)))
                badimages[img] = dict(imagestatus=e.code)
        return badimages

    def checkDomain(self, spUUID):
        domainstatus = 0
        message = "Domain is OK"
        badimages = {}
        try:
            self.validate()
            badimages = self.checkImages(spUUID)
            if badimages:
                message = "Domain has bad images"
                domainstatus = se.StorageDomainCheckError.code
        except se.StorageException, e:
            misc.logException(e)
            domainstatus = e.code
            message = str(e)
        except:
            domainstatus = se.StorageException.code
            message = "Domain error"
        return dict(domainstatus=domainstatus, badimages=badimages, message=message)

    def imageGarbageCollector(self):
        """
        Image Garbage Collector
        remove the remnants of the removed images (they could be left sometimes
        (on NFS mostly) due to lazy file removal
        """
        pass
